/*
Copyright 2021 YE Haifeng.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package controllers

import (
	"context"

	"github.com/go-logr/logr"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/tools/record"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	"sigs.k8s.io/controller-runtime/pkg/handler"
	"sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/source"

	sysv1 "gmap/api/v1"
)

// GmapReconciler reconciles a Gmap object
type GmapReconciler struct {
	client.Client
	Scheme   *runtime.Scheme
	Recorder record.EventRecorder
}

// const (
// 	//disableAnnotation string = "disable-globalconfig"
// 	nSelector string = "sys.k8s.99bill.com/gmap"
// )

//+kubebuilder:rbac:groups=sys.k8s.99bill.com,resources=gmaps,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=sys.k8s.99bill.com,resources=gmaps/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=sys.k8s.99bill.com,resources=gmaps/finalizers,verbs=update
//+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups="",resources=configmaps/finalizers,verbs=update
//+kubebuilder:rbac:groups="",resources=namespaces,verbs=get;list;watch

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the Gmap object against the actual cluster state, and thens
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.8.3/pkg/reconcile
func (r *GmapReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	log := log.FromContext(ctx)

	// your logic here
	log.Info("Request info", "Namespace", req.Namespace, "Name", req.Name)
	g := &sysv1.Gmap{}

	if err := r.Get(ctx, req.NamespacedName, g); err != nil {
		return ctrl.Result{}, client.IgnoreNotFound(err)
	}

	// r.Recorder.Eventf(g, corev1.EventTypeWarning, "Error", "some error")

	// 这里我们自定义一个 finalizer 字段
	myFinalizerName := "yhf.finalizers.sys.k8s.99bill.com"

	if g.ObjectMeta.DeletionTimestamp.IsZero() {
		//这里由于 DeletionTimestamp 是0，即没有删除操作，则不进行处理，只检查 CR 中是否含有自定义 finalizer 字符，若没有则增加。
		if !containsString(g.ObjectMeta.Finalizers, myFinalizerName) {
			g.ObjectMeta.Finalizers = append(g.ObjectMeta.Finalizers, myFinalizerName)
			g.Status.LastUpdateTime = metav1.Now()
			if err := r.Update(ctx, g); err != nil {
				log.Error(err, "Add Finalizers error", "name", g.Name)
				return ctrl.Result{}, err
			}
		}
	} else {
		//进行预删除操作
		if containsString(g.ObjectMeta.Finalizers, myFinalizerName) {
			// do something
			if err := r.PreDelete(ctx, log, g); err != nil {
				// if fail to delete the external dependency here, return with error
				// so that it can be retried
				return ctrl.Result{}, err
			}
			// 从 CR 中删除自定义 finalizer 字段。
			g.ObjectMeta.Finalizers = removeString(g.ObjectMeta.Finalizers, myFinalizerName)
			g.Status.LastUpdateTime = metav1.Now()
			if err := r.Update(ctx, g); err != nil {
				log.Error(err, "Remove Finalizers error", "name", g.Name)
				return ctrl.Result{}, err
			}
		}
		return ctrl.Result{}, nil
	}

	// to check the ConfigMap of the Owner
	cmName := g.Spec.Name
	data := g.Spec.Data

	listNS := corev1.NamespaceList{}

	opts := client.ListOptions{
		Limit:         1000,
		LabelSelector: labels.SelectorFromSet(labels.Set{NS_LABEL_KEY: "true"}),
	}
	if err := r.List(ctx, &listNS, &opts); err != nil {
		log.Error(err, "Get NameSpace failed.")
		return ctrl.Result{}, err
	}
	for _, ns := range listNS.Items {
		cm := corev1.ConfigMap{}
		if err := r.Get(ctx, types.NamespacedName{Namespace: ns.Name, Name: cmName}, &cm); err != nil {
			// Create the ConfigMap if it is not exist
			if errors.IsNotFound(err) {
				new := corev1.ConfigMap{
					ObjectMeta: metav1.ObjectMeta{
						Name:        cmName,
						Namespace:   ns.Name,
						Labels:      g.Spec.Labels,
						Annotations: g.Spec.Annotations,
					},
					Data: data,
				}
				if err := controllerutil.SetControllerReference(g, &new, r.Scheme); err != nil {
					log.Error(err, "set controllerReference failed", "name", new.Name, "namespace", new.Namespace)
					continue
				}
				if err := r.Create(ctx, &new); err != nil {
					log.Error(err, "Create ConfigMap failed.", "name", new.Name, "namespace", new.Namespace)
					continue
				}

				log.Info("Create ConfigMap success.", "namespace", new.Namespace, "name", new.Name)
				continue
			}

			log.Error(err, "Get ConfigManp failed.", "nameSpace", ns.Name, "name", cmName)
			continue
		}

		// Check GRAY ANNOTATION
		if ContainsAnnotation(&ns, GRAY_ANNOTATION) {
			continue
		}

		// Conpare and Update
		if !compareData(cm.Data, data) {
			new := corev1.ConfigMap{
				ObjectMeta: cm.DeepCopy().ObjectMeta,
				Data:       data,
			}
			if err := r.Update(ctx, &new); err != nil {
				log.Error(err, "Update ConfigMap failed.", "name", new.Name, "namespace", new.Namespace)
				continue
			}
			log.Info("Update ConfigMap success.", "namespace", new.Namespace, "name", new.Name)
			continue
		}
	}

	return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *GmapReconciler) SetupWithManager(mgr ctrl.Manager) error {
	// cmeta := metav1.ObjectMeta{
	// 	Namespace: "lab",
	// 	Labels: map[string]string{
	// 		nSelector: "true",
	// 	},
	// }

	nsmeta := metav1.ObjectMeta{
		Labels: map[string]string{
			NS_LABEL_KEY: "true",
		},
	}

	return ctrl.NewControllerManagedBy(mgr).
		For(&sysv1.Gmap{}).
		Owns(&corev1.ConfigMap{}).
		// Watches(&source.Kind{Type: &corev1.ConfigMap{ObjectMeta: cmeta}}, &handler.EnqueueRequestForObject{}).
		Watches(&source.Kind{Type: &corev1.Namespace{ObjectMeta: nsmeta}}, &handler.EnqueueRequestForObject{}).
		WithEventFilter(&ResourceAnnotationPredicate{}).
		Complete(r)
}

// Helper functions to check and remove string from a slice of strings.
func containsString(slice []string, s string) bool {
	for _, item := range slice {
		if item == s {
			return true
		}
	}
	return false
}

func removeString(slice []string, s string) (result []string) {
	for _, item := range slice {
		if item == s {
			continue
		}
		result = append(result, item)
	}
	return
}

// Unit pre delete logic
func (r *GmapReconciler) PreDelete(ctx context.Context, log logr.Logger, instance *sysv1.Gmap) error {
	log.Info("finding existing ConfigMaps for MyKind resource")
	return nil
}
